package sc.datainterchange;


import java.io.IOException;

/**
 * Encodes and writes protocol message fields.
 * 
 * @author stephencarmody@gmail.com Stephen Carmody
 */
public final class CodedOutputBuffer
{
	private final byte[] buffer;
	private final int limit;
	private int position;

	/**
	 * Create a new {@code CodedOutputBuffer} wrapping the given
	 * {@code OutputStream} with a given buffer size.
	 */
	public CodedOutputBuffer(byte[] buffer)
	{
		this.buffer = buffer;
		this.position = 0;
		this.limit = buffer.length;
	}
	
	/**
	 * Resets the current position of the buffer to zero.
	 */
	public void clear()
	{
		this.position = 0;
	}
	
	/**
	 * Gets the current position of the buffer.
	 */
	public int position()
	{
		return this.position;
	}

	/** Write a {@code double} field, including tag, to the stream. */
	public void writeDouble(double value) throws IOException
	{
		writeRawLittleEndian64(Double.doubleToRawLongBits(value));
	}

	/** Write a {@code float} field, including tag, to the stream. */
	public void writeFloat(float value) throws IOException
	{
		writeRawLittleEndian32(Float.floatToRawIntBits(value));
	}

	/** Write a {@code uint64} field, including tag, to the stream. */
	public void writeUInt64(long value) throws IOException
	{
		writeRawVarint64(value);
	}

	/** Write an {@code int64} field, including tag, to the stream. */
	public void writeInt64(long value) throws IOException
	{
		writeRawVarint64(value);
	}

	/** Write an {@code int32} field, including tag, to the stream. */
	public void writeInt32(int value) throws IOException
	{
		if (value >= 0)
		{
			writeRawVarint32(value);
		}
		else
		{
			// Must sign-extend.
			writeRawVarint64(value);
		}
	}

	/** Write a {@code fixed64} field, including tag, to the stream. */
	public void writeFixed64(long value) throws IOException
	{
		writeRawLittleEndian64(value);
	}

	/** Write a {@code fixed32} field, including tag, to the stream. */
	public void writeFixed32(int value) throws IOException
	{
		writeRawLittleEndian32(value);
	}

	/** Write a {@code bool} field, including tag, to the stream. */
	public void writeBool(boolean value) throws IOException
	{
		writeRawByte(value ? 1 : 0);
	}

	/** Write a {@code string} field, including tag, to the stream. */
	public void writeString(String value) throws IOException
	{
		// Unfortunately there does not appear to be any way to tell Java to
		// encode
		// UTF-8 directly into our buffer, so we have to let it create its own
		// byte
		// array and then copy.
		byte[] bytes = value.getBytes("UTF-8");
		writeRawVarint32(bytes.length);
		writeRawBytes(bytes);
	}

	/**
	 * Write a tag to the stream.
	 */
	public void writeTag(int tag) throws IOException
	{
		if (tag == 0)
			throw new IOException("Invalid tag");
		writeRawVarint32(tag);
	}
	
	/**
	 * Write a {@code uint32} field, including tag, to the stream.
	 */
	public void writeUInt32(int value) throws IOException
	{
		writeRawVarint32(value);
	}

	/**
	 * Write an enum field, including tag, to the stream. Caller is responsible
	 * for converting the enum value to its numeric value.
	 */
	public void writeEnum(int value) throws IOException
	{
		writeRawVarint32(value);
	}

	/** Write an {@code sfixed32} field, including tag, to the stream. */
	public void writeSFixed32(int value) throws IOException
	{
		writeRawLittleEndian32(value);
	}

	/** Write an {@code sfixed64} field, including tag, to the stream. */
	public void writeSFixed64(long value) throws IOException
	{
		writeRawLittleEndian64(value);
	}

	/** Write an {@code sint32} field, including tag, to the stream. */
	public void writeSInt32(int value) throws IOException
	{
		writeRawVarint32(encodeZigZag32(value));
	}

	/** Write an {@code sint64} field, including tag, to the stream. */
	public void writeSInt64(long value) throws IOException
	{
		writeRawVarint64(encodeZigZag64(value));
	}
	
	/**
	 * Return the space left in the array.
	 */
	public int spaceLeft()
	{
		return limit - position;
	}

	/**
	 * Verifies that {@link #spaceLeft()} returns zero. It's common to create a
	 * byte array that is exactly big enough to hold a message, then write to it
	 * with a {@code CodedOutputStream}. Calling {@code checkNoSpaceLeft()}
	 * after writing verifies that the message was actually as big as expected,
	 * which can help catch bugs.
	 */
	public void checkNoSpaceLeft()
	{
		if (spaceLeft() != 0)
		{
			throw new IllegalStateException(
					"Did not write as much data as expected.");
		}
	}

	/** Write a single byte. */
	public void writeRawByte(byte value) throws IOException
	{
		if (position == limit)
			throw new IOException("The current limit was reached");

		buffer[position++] = value;
	}

	/** Write a single byte, represented by an integer value. */
	public void writeRawByte(int value) throws IOException
	{
		writeRawByte((byte) value);
	}

	/** Write an array of bytes. */
	public void writeRawBytes(byte[] value) throws IOException
	{
		writeRawBytes(value, 0, value.length);
	}

	/** Write part of an array of bytes. */
	public void writeRawBytes(byte[] value, int offset, int length)
			throws IOException
	{
		if (limit - position < length)
			throw new IOException("The current limit was reached");
		
		// We have room in the current buffer.
		System.arraycopy(value, offset, buffer, position, length);
		position += length;
	}

	/**
	 * Encode and write a varint. {@code value} is treated as unsigned, so it
	 * won't be sign-extended if negative.
	 */
	public void writeRawVarint32(int value) throws IOException
	{
		while (true)
		{
			if ((value & ~0x7F) == 0)
			{
				writeRawByte(value);
				return;
			}
			else
			{
				writeRawByte((value & 0x7F) | 0x80);
				value >>>= 7;
			}
		}
	}

	/** Encode and write a varint. */
	public void writeRawVarint64(long value) throws IOException
	{
		while (true)
		{
			if ((value & ~0x7FL) == 0)
			{
				writeRawByte((int) value);
				return;
			}
			else
			{
				writeRawByte(((int) value & 0x7F) | 0x80);
				value >>>= 7;
			}
		}
	}

	/** Write a little-endian 32-bit integer. */
	public void writeRawLittleEndian32(int value) throws IOException
	{
		writeRawByte((value) & 0xFF);
		writeRawByte((value >> 8) & 0xFF);
		writeRawByte((value >> 16) & 0xFF);
		writeRawByte((value >> 24) & 0xFF);
	}

	public static final int LITTLE_ENDIAN_32_SIZE = 4;

	/** Write a little-endian 64-bit integer. */
	public void writeRawLittleEndian64(long value) throws IOException
	{
		writeRawByte((int) (value) & 0xFF);
		writeRawByte((int) (value >> 8) & 0xFF);
		writeRawByte((int) (value >> 16) & 0xFF);
		writeRawByte((int) (value >> 24) & 0xFF);
		writeRawByte((int) (value >> 32) & 0xFF);
		writeRawByte((int) (value >> 40) & 0xFF);
		writeRawByte((int) (value >> 48) & 0xFF);
		writeRawByte((int) (value >> 56) & 0xFF);
	}

	public static final int LITTLE_ENDIAN_64_SIZE = 8;

	/**
	 * Encode a ZigZag-encoded 32-bit value. ZigZag encodes signed integers into
	 * values that can be efficiently encoded with varint. (Otherwise, negative
	 * values must be sign-extended to 64 bits to be varint encoded, thus always
	 * taking 10 bytes on the wire.)
	 * 
	 * @param n
	 *            A signed 32-bit integer.
	 * @return An unsigned 32-bit integer, stored in a signed int because Java
	 *         has no explicit unsigned support.
	 */
	public static int encodeZigZag32(int n)
	{
		// Note: the right-shift must be arithmetic
		return (n << 1) ^ (n >> 31);
	}

	/**
	 * Encode a ZigZag-encoded 64-bit value. ZigZag encodes signed integers into
	 * values that can be efficiently encoded with varint. (Otherwise, negative
	 * values must be sign-extended to 64 bits to be varint encoded, thus always
	 * taking 10 bytes on the wire.)
	 * 
	 * @param n
	 *            A signed 64-bit integer.
	 * @return An unsigned 64-bit integer, stored in a signed int because Java
	 *         has no explicit unsigned support.
	 */
	public static long encodeZigZag64(long n)
	{
		// Note: the right-shift must be arithmetic
		return (n << 1) ^ (n >> 63);
	}
}
